/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.commons.lunar;

import java.io.DataInputStream;
import java.io.InputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

/**
 *
 * lunar.dat 数据格式：
 * <pre>
 * offset   size    content
 * 0        4       开始年份
 * 4        4       年数
 * 8        4*N     农历年份信息数据
 *
 *
 * 农历年份信息数据
 * 每个农历年占32位（1个int值），格式：
 * ---- ---- -FFF FFFM MMMM MMMM MMMD LLLL
 *
 * F    6位，该年春节所在公历年元旦至春节(农历正月初一)的天数
 * M    12位，每位表示该年一月至十二月的天数，为1表示30天，为0表示29天，一月在最高位；
 * D    1位，若该年有闰月则表示闰月的天数，为1表示30天，为0表示29天；
 * L    4位，该年的闰月，如果为4位全是0则表示该年没有闰月；
 * -    9位，未使用。
 * </pre>
 *
 * @author HeDYn<hedyn@foxmail.com>
 */
class Lunardat {

    static Lunardat INSTANCE = new Lunardat();

    private int[] LUNAR_ARR;
    private int START_LUNAR_YEAR;
    private int END_LUNAR_YEAR;
    private GregorianCalendar START_CALENDAR;
    private GregorianCalendar END_CALENDAR;

    private Lunardat() {
        InputStream is = getClass().getResourceAsStream("lunar.dat");
        DataInputStream dis = new DataInputStream(is);
        try {
            START_LUNAR_YEAR = dis.readInt();
            int len = dis.readInt();
            LUNAR_ARR = new int[len];
            for (int i = 0; i < len; i++) {
                LUNAR_ARR[i] = dis.readInt();
            }
            END_LUNAR_YEAR = START_LUNAR_YEAR + LUNAR_ARR.length - 1;
        } catch (Exception e) {
            throw new IllegalStateException("Loading lunar.dat error.");
        } finally {
            try {
                dis.close();
            } catch (Exception e) {
            }
        }

        START_CALENDAR = getSpringFestivalCalendar(START_LUNAR_YEAR);
        END_CALENDAR = getSpringFestivalCalendar(END_LUNAR_YEAR);
        END_CALENDAR.add(Calendar.DAY_OF_MONTH, daysOfYear(lunarData(END_LUNAR_YEAR)) - 1);
    }

    int getStartLunarYear() {
        return Lunardat.INSTANCE.START_LUNAR_YEAR;
    }

    int getEndLunarYear() {
        return Lunardat.INSTANCE.END_LUNAR_YEAR;
    }

    Date getStratDate() {
        return Lunardat.INSTANCE.START_CALENDAR.getTime();
    }

    Date getEndDate() {
        return Lunardat.INSTANCE.END_CALENDAR.getTime();
    }

    Calendar convertToCalendar(LunarDate date) {
        int year = date.getYear();
        if (year < START_LUNAR_YEAR || year > END_LUNAR_YEAR) {
            return null;
        }
        int v = LUNAR_ARR[year - START_LUNAR_YEAR];
        int month = date.getMonth();
        int days = 0;
        if (date.isLeapMonth()) {
            for (int i = 1; i <= month; i++) {
                days += valueOfM(v, i) ? 30 : 29;
            }
        } else {
            for (int i = 1; i < month; i++) {
                days += valueOfM(v, i) ? 30 : 29;
            }
            if (month > valueOfL(v)) {
                days += valueOfD(v);
            }
        }
        days += date.getDay();
        days--;

        Calendar calendar = getSpringFestivalCalendar(year);
        calendar.add(Calendar.DAY_OF_MONTH, days);
        return calendar;
    }

    LunarDate convertToLunarDate(Calendar calendar) {
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        Calendar tempCalendar = Calendar.getInstance();
        tempCalendar.set(year, month, day, 0, 0, 0);
        tempCalendar.clear(Calendar.MILLISECOND);
        if (tempCalendar.before(START_CALENDAR) || tempCalendar.after(END_CALENDAR)) {
            return null;
        }

        int diff = compareToSpringFestival(tempCalendar);
        if (diff == 0) {
            return new LunarDate(year, 1, 1, false);// 春节
        }
        int v;
        if (diff < 0) {
            year--;
            v = LUNAR_ARR[year - START_LUNAR_YEAR];
            diff += daysOfYear(v);
        } else {
            v = LUNAR_ARR[year - START_LUNAR_YEAR];
        }
        int leapMonth = valueOfL(v);
        int lunarMonth = 1;
        int tmp = 0;
        boolean isLeap;
        while (lunarMonth <= 12) {
            isLeap = false;
            int days = valueOfM(v, lunarMonth) ?  30 : 29;
            tmp += days;

            // 闰月
            if (tmp <= diff && lunarMonth == leapMonth) {
                isLeap  = true;
                days = valueOfD(v);
                tmp += days;
            }

            if (tmp > diff) {
                tmp -= days;    // 回退上一步加上的天数
                int lunarDay = diff - tmp + 1;
                return new LunarDate(year, lunarMonth, lunarDay, isLeap);
            }
            lunarMonth++;
        }
        return null;
    }

    private GregorianCalendar getSpringFestivalCalendar(int year) {
        GregorianCalendar calendar = new GregorianCalendar();
        int month = 0;
        int day = valueOfF(lunarData(year));
        if (day > 31) {
            month = 1;
            day -= 31;
        }
        calendar.set(year, month, day, 0, 0, 0);
        return calendar;
    }

    /**
     * 获取指定公历日期所处的农历年份的春节到该日期的天数
     * @param calendar
     * @return
     */
    private int compareToSpringFestival(Calendar calendar) {
        int year = calendar.get(Calendar.YEAR);
        int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
        int sfDays;
        if (year > END_LUNAR_YEAR) {
            sfDays = END_CALENDAR.get(Calendar.DAY_OF_YEAR) + 1;
        } else {
            sfDays = valueOfF(lunarData(year));
        }
        return dayOfYear - sfDays;
    }

    private int lunarData(int year) {
        return LUNAR_ARR[year - START_LUNAR_YEAR];
    }

    /**
     *
     * @param v
     * @return
     */
    private int valueOfF(int v) {
        return v >> 17;
    }

    /**
     * 判断指定农历月份是否是大月（30天）
     * @param v 数据值
     * @param month 农历月份，从 1 开始，即正月为 1
     * @return
     */
    private boolean valueOfM(int v, int month) {
        return ((v >> (17 - month)) & 1) != 0;
    }

    /**
     * 获取闰月
     * @param v
     * @return
     */
    private int valueOfL(int v) {
        return v & 0xf;
    }

    /**
     * 获取闰月的天数
     * @param v
     * @return
     */
    private int valueOfD(int v) {
        if (valueOfL(v) != 0) {
            return ((v >> 4) & 1) != 0 ? 30 : 29;
        }
        return 0;
    }

    private int daysOfYear(int v) {
        int days = 0;
        for (int i = 1; i <= 12; i++) {
            days += valueOfM(v, i) ? 30 : 29;
        }
        days += valueOfD(v);
        return days;
    }
}
